package com.ar4j.sql;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.JdbcUtils;

import com.ar4j.ActiveRecord;
import com.ar4j.ActiveRecordContext;
import com.ar4j.NamedSingletonActiveRecordFactory;
import com.ar4j.ActiveRecordMetadata;
import com.ar4j.type.ITypeConverter;
import com.ar4j.util.ClassUtils;

/**
 * A row mapper capable of mapping the contents of a ResultSet row to an active record instance.
 */
public class ActiveRecordRowMapper<T extends ActiveRecord<T>> implements RowMapper<T>  {
  private String factoryName;
  private Class<?> baseClass;
  private Map<String, Class<?>> propertyTypeMap;
  private Map<String, ITypeConverter<?, ?>> propertyTypeConvertorMap;
  private Map<String, String> columnToPropertyMap;
  private ActiveRecordContext prototypeContext;

  /**
   * Create a new mapper using the active record's metadata and a given prototype context.
   */
  public ActiveRecordRowMapper(ActiveRecordMetadata metadata, ActiveRecordContext prototypeContext) {
    this.factoryName = metadata.getFactoryName();
    this.baseClass = metadata.getBaseClass();
    this.propertyTypeMap = metadata.getPropertyTypeMap();
    this.propertyTypeConvertorMap = metadata.getPropertyTypeConvertorMap();
    this.prototypeContext = prototypeContext;
    
    columnToPropertyMap = new HashMap<String, String>();
    for(Map.Entry<String, String> entry : metadata.getPropertyColumnNameMap().entrySet()) {
      columnToPropertyMap.put(entry.getValue(), entry.getKey());
    }
  }

  /**
   * Create a new active record instance, settings its properties to values retrieved from the
   * given result set. Conversions and type coercions are performed as needed.
   */
  @SuppressWarnings("unchecked")
  @Override
  public T mapRow(ResultSet rs, int rowNum) throws SQLException {
    
    // map column indexes to property names
    ResultSetMetaData rsmd = rs.getMetaData();
    int columnCount = rsmd.getColumnCount();
    Map<Integer, String> indexToPropertyMap = new HashMap<Integer, String>();
    Map<Integer, Class<?>> indexToPropertyTypeMap = new HashMap<Integer, Class<?>>();
    for(int i = 1; i <= columnCount; i++) {
      String column = JdbcUtils.lookupColumnName(rsmd, i).toLowerCase();
      String property = columnToPropertyMap.get(column);
      if(property == null) {
        throw new SQLException("Unknown column in result set: '" + column + " for active record: " + baseClass.getName());
      }
      
      indexToPropertyMap.put(i, property);
      indexToPropertyTypeMap.put(i, propertyTypeMap.get(property));
    }
    
    // map the row
    T out = NamedSingletonActiveRecordFactory.getFactory(factoryName).getActiveRecord((Class<T>) baseClass);
    out.getContext().setValuesFrom(prototypeContext);
    for(int i = 1; i <= columnCount; i++) {
      String property = indexToPropertyMap.get(i);
      Class<?> propertyType = indexToPropertyTypeMap.get(i);
      Class<?> storageType = propertyType;
      
      // see if we have a converter and use its storage type if needed
      ITypeConverter<Object, Object> converter = (ITypeConverter<Object, Object>) propertyTypeConvertorMap.get(property);
      if(converter != null) {
        storageType = converter.getStorageType();
      }
      
      // extract value from the result set and use the specified converter if needed
      Object value = SqlUtils.extractValueFromResultSet(rs, i, storageType);
      if(converter != null) {
        value = converter.convertFromStorage(value);
      } 
      
      // do a final coercion to the property type and set its value
      value = ClassUtils.coerceValueToType(value, propertyType);
      out.setProperty(property, value);      
    }
    
    return out;
  }
  
  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
  }
}
